import type { MultipartParserOptions } from '@mjackson/multipart-parser';
import {
  isMultipartRequest,
  parseMultipartRequest,
  MultipartPart,
} from '@mjackson/multipart-parser';

/**
 * The base class for errors thrown by the form data parser.
 */
export class FormDataParseError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'FormDataParseError';
  }
}

/**
 * An error thrown when the maximum number of files allowed in a request is exceeded.
 */
export class MaxFilesExceededError extends FormDataParseError {
  constructor(maxFiles: number) {
    super(`Maximum number of files exceeded: ${maxFiles}`);
    this.name = 'MaxFilesExceededError';
  }
}

/**
 * A file that was uploaded as part of a `multipart/form-data` request.
 */
export class FileUpload extends File {
  /**
   * The name of the <input> field used to upload the file.
   */
  readonly fieldName: string;

  constructor(part: MultipartPart, fieldName: string) {
    super(part.content, part.filename ?? 'file-upload', {
      type: part.mediaType ?? 'application/octet-stream',
    });

    this.fieldName = fieldName;
  }
}

/**
 * A function used for handling file uploads.
 */
export interface FileUploadHandler {
  (file: FileUpload): void | null | string | Blob | Promise<void | null | string | Blob>;
}

async function defaultFileUploadHandler(file: FileUpload): Promise<File> {
  // By default just keep the file around in memory.
  return file;
}

export interface ParseFormDataOptions extends MultipartParserOptions {
  /**
   * The maximum number of files that can be uploaded in a single request.
   * If this limit is exceeded, a `MaxFilesExceededError` will be thrown.
   *
   * Default: 20
   */
  maxFiles?: number;
}

/**
 * Parses a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) body into a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
 * object. This is useful when accessing the data contained in a HTTP `multipart/form-data` request
 * generated by a HTML `<form>` element.
 *
 * This is a drop-in replacement for [the built-in `request.formData()` API](https://developer.mozilla.org/en-US/docs/Web/API/Request/formData)
 * with the main difference being the ability to customize the handling of file uploads. Instead of
 * keeping all files in memory, the `uploadHandler` allows you to store the file on disk or a
 * cloud storage service.
 *
 * @param request The `Request` object to parse
 * @param uploadHandler A function that handles file uploads. It receives a `File` object and may return any value that is valid in a `FormData` object
 * @return A `Promise` that resolves to a `FormData` object containing the parsed data
 */
export async function parseFormData(
  request: Request,
  uploadHandler?: FileUploadHandler,
): Promise<FormData>;
export async function parseFormData(
  request: Request,
  options: ParseFormDataOptions,
  uploadHandler?: FileUploadHandler,
): Promise<FormData>;
export async function parseFormData(
  request: Request,
  options?: ParseFormDataOptions | FileUploadHandler,
  uploadHandler: FileUploadHandler = defaultFileUploadHandler,
): Promise<FormData> {
  if (typeof options === 'function') {
    uploadHandler = options;
    options = {};
  } else if (options == null) {
    options = {};
  }

  if (!isMultipartRequest(request)) {
    return request.formData();
  }

  let { maxFiles = 20, ...parserOptions } = options;

  let formData = new FormData();
  let fileCount = 0;

  for await (let part of parseMultipartRequest(request, parserOptions)) {
    let fieldName = part.name;
    if (!fieldName) continue;

    if (part.isFile) {
      if (++fileCount > maxFiles) {
        throw new MaxFilesExceededError(maxFiles);
      }

      let value = await uploadHandler(new FileUpload(part, fieldName));
      if (value != null) {
        formData.append(fieldName, value);
      }
    } else {
      formData.append(fieldName, part.text);
    }
  }

  return formData;
}
